Loading AI tools
来自维基百科,自由的百科全书
Common Lisp,縮寫為CL(不是組合邏輯的縮寫)是Lisp程式語言的一種方言,由ANSI INCITS 226-1994(R2004)(前身為ANSI X3.226-1994(R1999)),所定義的語言規範標準。Common Lisp HyperSpec是源自於ANSI Common Lisp標準的網頁超連結版本。
![]() | |
編程範型 | 多範式:程序式, 函數式, 物件導向, 元程式設計, 反射式, 泛型 |
---|---|
設計者 | Scott Fahlman, Richard P. Gabriel, David A. Moon, Kent Pitman, Guy Steele, Dan Weinreb |
實作者 | ANSI X3J13委員會 |
釋出時間 | 1984年 | , ANSI Common Lisp:1994年
目前版本 | |
型態系統 | 動態型別、強型別 |
作業系統 | 跨平台 |
特許條款 | GNU通用公眾特許條款、Artistic License |
副檔名 | .lisp, .lsp, .l, .cl, .fasl |
網站 | common-lisp |
啟發語言 | |
Lisp, Lisp Machine Lisp, Maclisp, Scheme, InterLisp | |
影響語言 | |
Clojure, Dylan, Emacs Lisp, EuLisp, ISLISP, R, SKILL, SubL, Julia |
CL語言是為標準化和改良Maclisp而開發的後繼者。到20世紀80年代初,幾個工作群組已經在設計MacLisp各種後繼者,例如:Lisp Machine Lisp(又名 ZetaLisp),Spice Lisp,NIL和S-1 Lisp。CL是為了標準化和擴展此前眾多的MacLisp分支而開發,它本身並非具體的實作,而是對語言設立標準的規範。有數個實作符合Common Lisp規範,其中包括自由和開源軟件,以及商業化產品。CL支援了結構化、函數式和物件導向編程等範式。相對於各種嵌入在特定產品中的語言,如Emacs Lisp和AutoLISP,Common Lisp是一種用途廣泛的程式語言。不同於很多早期Lisp,Common Lisp如同Scheme,其中的變數是預設為詞法作用域的。
身為一種動態程式語言,它有助於進化和增量的軟件開發,並將其迭代編譯成高效的執行程式。這種增量開發通常是互動持續地改善,而不需中斷執行中的應用程式。它還支援在後期的分析和優化階段添加可選的型別註記與轉型,使編譯器產生更有效率的代碼。例如在硬件和實作的支援範圍內,fixnum
能儲存一個未封裝整數,允許比大整數或任意精度類型更高效率的運算。同樣地,在每個模組或函數的基礎上可聲明優化,指示編譯器要編譯成哪一類型的安全級別。
CL包含了支援多分派和方法組合的物件系統,縮寫為CLOS,它通常以元物件(Metaobject)協定來實現。
CL藉由標準功能進行擴展,例如Lisp宏(編譯時期程式自身完成的代碼重排(compile-time code rearrangement accomplished by the program itself))和閱讀器宏(賦予用戶自訂的語法以擴充具特殊意義的符號(extension of syntax to give special meaning to characters reserved for users for this purpose))。
CL為Maclisp和約翰·麥卡錫的原創Lisp提供了一些向下相容性。這允許較舊的Lisp軟件移植到Common Lisp之上。
在1981年,ARPA管理者Bob Engelmore最初發起了關於Common Lisp的工作,開發一個單一的社群標準Lisp方言[1]。大多數最初的語言設計是通過電子郵件完成的[2][3]。在1982年,Guy L. Steele Jr.於1982年度LISP和函數式程式設計研討會上首次給出了Common Lisp的概述[4]。
在1984年,首個語言文件被出版為《Common Lisp語言》,第一版(也叫做CLtL1)。在1990年,第二版(也叫做CLtL2)出版了,它結合了在ANSI Common Lisp標準化過程中對語言做的很多變更:擴充的LOOP
語法、Common Lisp對象系統、用於錯誤處理的條件系統、到精美列印的介面等等。但是CLtL2不描述最終的ANSI Common Lisp標準,因而不是ANSI Common Lisp的文件。在1994年,最終的ANSI Common Lisp標準出版了。自從出版之後標準就沒有更新。對Common Lisp的各種擴充和改進(例如Unicode、並行、基於CLOS的IO)都由實現和函式庫來提供。
Common Lisp是Lisp編程語族的一種方言; 它使用S-表達式來表示原始碼和資料結構。函數呼叫、宏形式和基本形式都以列表來編寫,列表的第一項是函數名稱,如以下範例:
(+ 2 2) ; 将 2 加上2 得 4。函數名稱為'+',在Lisp語法中是唯一的(只能作用於數值)。
(defvar *x*) ; 先確保 *x* 變量存在,尚未賦值給它。星號也是變量名稱的一部份,
; 依慣例約定表示一個特殊(全局)變量。符號 *x* 與生俱有的屬性是
; 對於它後續的綁定是動態可變的,而非詞法靜止不變的。
(setf *x* 42.1) ; 對 *x* 變量賦予浮點數值 42.1。
;; 定义计算一个数的平方函数:
(defun square (x)
(* x x))
;; 执行这个函数:
(square 3) ; 返回平方值 9
;; 'let'構造為區域變量創建一個作用域。這裡變量'a' 被綁定到 6,變量'b'被綁定到 4。
;; 'let'的內部是一個函式體,對它求值後會返回最後一個計算值。這個'let'表達式將
;; a 和 b 相加的結果返回。變量 a 和 b 只存在於詞法作用域中,除非它們已先被標記
;; 成特殊變量(例如上述的 DEFVAR)。
(let ((a 6)
(b 4))
(+ a b)) ; 返回數值 10
Common Lisp 擁有豐富的數據類別。
數值型別包括整數,分數,浮點數和複數。Common Lisp使用大數(bignums)來表示任意大小和精度的數值。
分數型別確切地代表分數,很多語言並不具備這種能力。Common Lisp會自動將數值轉換成適當的型別。有許多方式取捨數值,函數round
將參數四捨六入為最接近的整數,逢五則取偶整數。truncate
,floor
和ceiling
分別朝向零,向下或向上取整數。所有這些函數將捨去的小數當作次要值返回。
例如,(floor -2.5)
產生 -3, 0.5;(ceiling -2.5)
產生 -2,-0.5;(round 2.5)
得到 2,0.5;和(round 3.5)
得到 4,-0.5。
Common Lisp字元型別不限於ASCII字元,因為在ASCII出現前Lisp就已經存在了。大多數現代實現允許Unicode字元。
符號(Symbol)型別是Lisp語言共有的,而在其它語言中較少見。一個符號是個命名唯一的對象,它擁有幾個部份:名稱,值,函數,屬性列表(property list)和套件。其中,值單元和函數單元是最重要的。Lisp中的符號通常類似於其它語言中的識別碼(identifier)用法:儲存變數的值;然而還有很多種用途。一般來說,對一個符號求值時會得到以該符號為變數名稱的值,但也有例外:譬如在關鍵符套件中的符號,形如:foo
的符號值就是它本身(自我評估的,self-evaluating),而符號T
和NIL
則用於表示布爾邏輯值真與假。Common Lisp可設計容納符號的命名空間,稱為「套件」(package)。
Common Lisp的序列型別包括列表、向量、位元向量和字串。有許多函數可對應不同型別的序列進行操作。
CL如同所有Lisp方言,列表由點對(conses)組成,有時稱為cons單元、序偶或構對。一個點對是帶有兩個儲存槽的數據結構,分別稱為car和cdr。列表就是一條點對的串列,或只是空串列。每一個點對的CAR會參照列表的成員(可能是另一個列表)。而除了最後一個的CDR參照到nil
值之外,其餘的CDR都會參照下一個點對。Conses也能輕易地實現樹和其它複雜的數據結構;儘管一般建議以結構體或是類別的實例來代替。利用點對能夠創建循環形的數據結構。
CL支援多維陣列,且如需要能動態地調整陣列大小。多維陣列常用於數學中的矩陣運算。向量就是一維陣列。陣列可載入任何型別(甚至於混合的型別)的成員,或只專用於特定某一型別的成員,例如由整數構成的位元向量。許多Lisp實作會根據特定型別,對陣列的操作函數進行優化。兩種特定型別的專用陣列是內建的:字串和位元向量。字串是由許多字元構成的向量,而位元向量是由許多位元構成的向量。
雜湊表儲存對象之間的關聯,任何物件都可以作為雜湊表的鍵或值。和陣列一樣,雜湊表可依需求自動調整其大小。
套件是一組符號的集合,主要用於將程式的個別部份區分命名空間。套件能匯出一些符號,將它們作為共用介面的某一部份,也可以匯入其它套件參照並概括承受其中的符號。
CL的結構體(Structures)類似於C語言的structs和Pascal的records,是一種任由用戶發揮的複雜數據結構定義,表示具有任意數量和任何型別的欄位(也叫做槽)。結構允許單一繼承。
類別(Class)在後期被整合進Common Lisp中,有些概念與結構體重疊,但類別提供了更多的動態特性和多重繼承(見 CLOS)。由類別創建的物件稱為實例。有個特殊情況是泛型(Generics)的雙重角色,泛型既是函數,也是類別的實例物件。
Common Lisp支援頭等函數(亦即函數可當成數據類型來處理)。例如編寫以其它函數當作一個函數的參數,或函數的傳回值也是函數,利用函數的結合來描述常用的操作。CL函式庫高度依賴於這樣的高階函數變換。舉例而言,sort
函數可將關係運算子作為參數,並選用如何取鍵的函數作為參數。如此一來不但能對任何型別的數據排序,還能根據取用的鍵值對數據結構作排序。
;; 使用大小於函數作為比較關係,對列表進行排序。
(sort (list 5 2 6 3 1 4) #'>) ; 大於比較排序結果 (6 5 4 3 2 1)
(sort (list 5 2 6 3 1 4) #'<) ; 小於比較排序結果 (1 2 3 4 5 6)
;; 對每個子列表中,根據其第一個元素作為鍵值,以小於比較關係來排序。
(sort (list '(9 A) '(3 B) '(4 C)) #'< :key #'first) ; 結果為 ((3 B) (4 C) (9 A))
對函數求值的模型非常簡單。當求值器遇到一個形式如(F a1 a2 ...)
時,那麼名稱為F
的符號會被假定是以下三種狀況之一:
lambda
符號開頭的子形式。)如果F
符號是三者其中之一,則求值器判定它是個函數,找到此函數的定義內容,然後以從左到右的次序來評估參數a1,a2,...,an
的值,並且使用這些值進行運算,以函數定義中最後一個評估的結果作為傳回值。
宏defun
用來定義函數。函數定義給出了函數名,參數名和函數體:
(defun square(x)
(* x x))
函數定義中可以包括「聲明」,它可以指示編譯器最佳化設置或參數的資料類型等。還可以在函數定義中包括「文件字串」(docstring),Lisp系統用它們形成互動式文件:
(defun square(x)
(declare (number x) (optimize (speed 3) (debug 0) (safety 1)))
"Calculates the square of the number x."
(* x x))
匿名函數用lambda
表達式定義。Lisp編程頻繁使用高階函數,以匿名函數作為其參數的作法十分有效。
還有一些有關於函數定義和函數操作的運算子。如,運算子compile
可以用來重新編譯函數。(一些Lisp系統預設下在直譯器里執行函數,除非指示編譯它;其他Lisp系統在函數輸入時即被編譯。)
defgeneric
宏用來定義泛化函數,而defmethod
宏則用來定義方法。泛化函數是一些方法的集合。方法可依照CLOS標準類別、系統類別、結構類別或物件,以特定方式處理它們所使用的參數。許多型別都有相對應的系統類別。當呼叫泛化函數數時,多樣派發(multiple-dispatch)將會依型別確定要應用的有效方法。如下列範例展示了對不同型別的參數如數值、向量或字串,設計對應的add
方法將兩個物件相加的動作。
(defgeneric add (a b))
(defmethod add ((a number) (b number))
(+ a b))
(defmethod add ((a vector) (b number))
(map 'vector (lambda (n) (+ n b)) a))
(defmethod add ((a vector) (b vector))
(map 'vector #'+ a b))
(defmethod add ((a string) (b string))
(concatenate 'string a b) )
(add 2 3) ; returns 5
(add #(1 2 3 4) 7) ; returns #(8 9 10 11)
(add #(1 2 3 4) #(4 3 2 1)) ; returns #(5 5 5 5)
(add "COMMON " "LISP") ; returns "COMMON LISP"
泛化函數也是第一類數據類別。除了上面陳述之外,泛化函數和方法還有更多的特性。
函數的名字空間與數據變數的名字空間是分離的。這是Common Lisp和Scheme程式語言的一個重要不同之處。在函數名字空間定義名字的運算子包括defun
、flet
和labels
。
要用函數名把函數作為參數傳給另一個函數,必須使用function
特殊運算子,通常簡略為#'
。上文第一個sort
的例子中,為了參照在函數名字空間名為>
的函數,使用了代碼#'>
。
Scheme程式語言的求值模型更簡單些:因為只有一個名字空間,式(form)中所有位置都被求值(以任意順序)-- 不僅是參數。所以以一種方言(dialect)寫就的代碼往往令熟悉其它方言程式設計師感到迷惑。例如,許多CL程式設計師喜歡使用描述性的變數名如"list"或"string",在Scheme中這將導致問題,因為它們可能局部覆蓋了函數名字。
為函數提供分離的名字空間是否有益是Lisp社區不斷爭論的主題之一,常被稱為「Lisp-1與Lisp-2辯論」。這些名稱出現於Richard P. Gabriel和Kent Pitman在1998年的一篇論文,其中廣泛的比較了這兩種方法。[5]
Common Lisp支援多值的概念,任何表達式經過評估之後必定會有一個主要值,但它也可能擁有任何數量的次要值,讓感興趣的呼叫者接收和檢查。這個概念與回傳列表值不同,因為次要值是備選用的,並通過專用的側面通道來傳遞。也就是說如果不需要次要值,則呼叫者完全不需要知道它們的存在,這是偶爾需使用額外而非必要的資訊,一個方便的機制。
TRUNCATE
函數對給定數值取最接近的整數。然而,它也會返回一個餘數作為次要值,使呼叫者確定有多少數值被捨棄了。它還支援可選用的除數參數,可顯明地表達帶餘除法:(let ((x 1266778)
(y 458))
(multiple-value-bind (quotient remainder)
(truncate x y)
(format nil "~A divided by ~A is ~A remainder ~A" x y quotient remainder)))
;;;; => "1266778 divided by 458 is 2765 remainder 408"
GETHASH
回傳雜湊表中依鍵作搜尋的值,否則返回預設值,還有一個指出是否找到該值的布爾輔助值。因此不論搜尋結果(找到鍵的對應值或預設值)是否成功,原始碼都可以直接使用它,但如果要求能區別搜尋結果的情況時,它可以檢查輔助的布爾值並做出適當反應。相同的函數調用支援兩種使用情境,不會受到另一個的負擔或約束影響。(defun get-answer (library)
(gethash 'answer library 42))
(defun the-answer-1 (library)
(format nil "The answer is ~A" (get-answer library)))
;;;; Returns "The answer is 42" if ANSWER not present in LIBRARY
(defun the-answer-2 (library)
(multiple-value-bind (answer sure-p)
(get-answer library)
(if (not sure-p)
"I don't know"
(format nil "The answer is ~A" answer))))
;;;; Returns "I don't know" if ANSWER not present in LIBRARY
一些標準形式支援多值,最常見的是用來存取次要值的MULTIPLE-VALUE-BIND
基本運算子和用於返回多值的VALUES
:
(defun magic-eight-ball ()
"Return an outlook prediction, with the probability as a secondary value"
(values "Outlook good" (random 1.0)))
;;;; => "Outlook good"
;;;; => 0.3187
Common Lisp中的其他數據類別包括:
read
函數)如何解析原始碼文字的物件型別。開發人員可操控Lisp編程在讀取原始碼時要使用哪一個讀取字表,改變或擴展Lisp的語法。與許多其它程式語言中的程式一樣,Common Lisp編程使用名稱來參照變數、函數和許多其它類型的實體。被命名的參照只在其作用域中有用。名稱與參照實體之間的關聯稱為綁定。作用域是指確定名稱具有特殊綁定的情況。
在Common Lisp中需要決定作用域的情況包括:
(go x)
表示將控制跳轉到x
標籤的位置,而(print x)
表示x
變數。這兩個x
的作用域可以在程式的相同區域處於活動狀態,因為tagbody
標籤的x
與x
變數名稱位於分開的命名空間中。基本運算子或宏形式可完全控制其語法中所有符號的含義。例如在(defclass x (a b) ())
表達式中,類別定義(a b)
是基本類別的列表,因此會在類別的命名空間中搜尋這些名稱;x
並非參照到現有的綁定,而是源自於a
和b
的新類別名稱。這些事實純粹由defclass
的語義表示得出。這表達式的唯一事實是defclass
參照一個宏綁定;其中的一切都由defclass
決定作用域。x
變數的參照被含括在一個綁定結構中,例如以let
綁定對x
的定義,則該參照的效用發生在該綁定創建的作用域內。special
,無論這聲明是在本地的或在全域中。這將使參照依據其位於詞法或動態的環境中來參照它。要理解符號參照到什麼實體,Common Lisp開發人員必須知道參照是屬於哪一種作用域,如果它是一個變數的參照,那它是處於什麼樣的(動態或詞法的)作用域中?以及在執行期的情況,參照在什麼環境中被參照,綁定是在哪裏被引入到環境等等。
Lisp中的一些環境總是存在於全域作用域之中, 例如定義了一個新型別,那麼以後在任何地方都會知道它。
該類型別的參照會從全域作用域中的環境去尋找。
環境在Common Lisp中有一種類型是動態環境。在這種環境中建立的綁定具有動態的作用域,這表示某些構造例如let
,會在執行的起點就先建立綁定,而在該構造完成執行時消失:它的生命週期依附着這區塊動態地觸發和停用。然而動態綁定不僅在該區塊中可見;對於從該區塊中調用的所有函數也是可見的。這樣的可見性被稱為不定的作用域。具有動態(依附區塊的觸發和停用相關的生命週期)和不定作用域(從該區塊調用的所有函數可見)的綁定,被稱為具有動態作用域。
Common Lisp支援動態作用域的變數,也稱為特殊變數。有些其它類型的綁定也必須是動態作用域的,例如重新啟動和捕獲標籤。函數綁定不能以flet
(僅提供詞法範圍的函數綁定)進行動態作用域,但可以將函數物件(Common Lisp中的第一類物件)分配給動態作用域的變數,在動態作用域內使用let
綁定,然後再以funcall
或APPLY
調用。
動態作用域非常有用,因為它將參照的清晰度和規律添加到全域變數中。電腦科學中的全域變數被認為是潛在的錯誤來源,因為它們可能導致模組之間存有特殊隱蔽的溝通渠道,從而導致令人驚訝而不在預期中的互動作用。
在Common Lisp中,只有頂層綁定的特殊變數就像其它程式語言中的全域變數一樣。它可以儲存一個新的值,該值僅替換頂層綁定中的值。造成使用全域變數的核心錯誤,是粗心的替代了全域變數值。但是,使用特殊變數的另一種方法是,在表達式中給它一個新的區域綁定。這有時被稱為「重新綁定」變數。動態作用域中對變數的綁定,會創建一個臨時的新記憶體位置給予該變數,並將該名稱與該位置相關聯。當該綁定有效,對該變數的所有參照都指向新的綁定;之前的綁定則是被隱藏起來的。當綁定表達式的執行結束時,臨時的記憶體位置消失,而舊綁定浮現出來,變數的原始值依舊完好無損。當然,同一變數的多個動態綁定可以巢狀。
在支援多緒的Common Lisp實作中,動態作用域是針對每個線程的。因此,特殊變數是當成線程區域儲存的抽象化。如果一個線程重新綁定了特殊變數,則此重新綁定對其它線程中的該變數沒有作用。儲存在綁定中的值只能由創建該綁定的線程取得。如果每個線程綁定一些特殊變數*x*
,則*x*
的行為就像線程在本地中儲存一樣。在沒有重新綁定*x*
的線程中,它的行為就像一個普通的全域變數:所有這些線程的參照都會指向*x*
的頂層綁定。
動態變數可以用來擴展執行上下文,並附加上下文訊息,這些資訊在函數之間隱含地傳遞,而不必顯示為額外的函數參數。當執行控制的轉移必須穿過不相關的代碼層時,不能藉由額外參數來擴展傳遞附加數據,所以這是非常有用的。這樣的情況通常需要一個全域變數,必須能夠被儲存和恢復,以便在遞歸時不會中斷:動態變數的重新綁定可以處理此情形。該變數必須是線程區域的(或必須使用大的互斥, mutex),因此這個情況不會在線程下斷開:動態作用域的實作也可以處理此情形。
在Common Lisp函式庫中有很多標準的特殊變數。例如,所有標準I/O流都儲存在頂層為眾所熟知的特殊變數的綁定中,即*standard-output*
。
假設有個foo函數寫入標準輸出:
(defun foo ()
(format t "Hello, world"))
要擷取其輸出中的字串,*standard-output*
可以被綁定到一個字串流,並調用它:
(with-output-to-string (*standard-output*)
(foo))
-> "Hello, world" ; gathered output returned as a string
Common Lisp支援詞法環境。形式上,詞法環境中的綁定具有詞法作用域,並可能具有不定的範圍或動態的範圍,取決於命名空間的類型。詞法作用域實際上表示可見性被限制在綁定建立的區塊中。參照沒有以文字(即詞法地)嵌入在該區塊中,根本看不到該綁定。
TAGBODY
中的標籤會具有詞法作用域。如果(GO X)
表達式實際上沒有嵌入到其中,則它會發生錯誤。TAGBODY
包含標籤X
。但是當TAGBODY
執行終了時,標籤的綁定就會消失,因為它們具有動態作用域。如果以調用一個詞法閉包重新進入該代碼區塊,那麼這個閉包的內文無法藉由GO
將控制轉移到標籤中:
(defvar *stashed*) ;; will hold a function
(tagbody
(setf *stashed* (lambda () (go some-label)))
(go end-label) ;; skip the (print "Hello")
some-label
(print "Hello")
end-label)
-> NIL
執行TAGBODY
時,它首先評估以setf
形式指向函數的特殊變數*stashed*
,然後(go end-label)
將控制項轉移到終了標籤,跳過代碼(print "Hello")
。由於終了標籤位於TAGBODY
的末端,於是終止並返回NIL
值。假設現在調用先前指向的函數:
(funcall *stashed*) ;; Error!
這種狀況是錯誤的。一個實作的錯誤回應該包含錯誤條件訊息,例如「GO: tagbody for tag SOME-LABEL has already been left」。該函數嘗試評估(go some-label)
,它是詞法地嵌入到TAGBODY
中並解析為標籤。然而TAGBODY
被跳過了而沒有執行(其作用域已經結束),故無法再轉移控制。
Lisp中的區域函數綁定具有詞法作用域,預設情況下變數綁定也同樣為詞法作用域。與GO
標籤對比,它們的作用域是範圍不定的。當一個詞法的函數或變數綁定時,既然可以對其參照參照,該綁定就會持續存在,即使在建立該綁定的結構已經終止後。參照到詞法變數和函數,在其建立結構終止後,可以藉由詞法的閉包來實現。
Common Lisp對於變數的預設模式是詞法綁定。對於個別符號可用區域聲明,或全域的聲明,來切換成動態作用域。而後者可能隱含地透過如DEFVAR
或DEFPARAMETER
,這樣的構造使符號成為全域可見的。Common Lisp編程中慣例以開頭和結尾星號*
,將特殊變數(即處於動態作用域的)包括起來,這稱為「耳罩慣例」。遵循此慣例的效果,即為特殊變數創建了一個單獨的命名空間,則應該處於詞法作用域的變數不會被意外地特殊化。
幾個原因使得詞法作用域有用。
首先,變數和函數的參照可以被編譯成高效的機械碼,因為執行期環境的結構相對簡單。在許多情況下它可以優化堆疊儲存,因此開啟和關閉的詞法作用域前置開銷最小。即使在必定要產生完整閉包的情況下,存取閉包的環境仍然是有效率的;每個變數通常會轉成一個綁定向量之中的偏移量,因此變數的參照就成為簡單的載入,或是以基底-加-偏移尋址模式表示的儲存指令。
其次詞法作用域(與不定範圍結合)可以創造出詞彙閉包,從而產生了中心以函數作為第一類物件的編程範式,這是函數式編程的根本。
第三,也許最重要的是,即使沒有用到詞法的閉包,詞法作用域的運用,會將程式模組與不需要的互動影響隔離開來。由於可見性受到限制,詞法變數是私有的。如果一個模組A綁定一個詞法變數X,並呼叫另一個模組B,則參照B其中的變數X,不會被意外地解析成在A中綁定的X。B根本無法存取X。對於需使用變數進行有規則的互動作用情況,Common Lisp提供了特殊變數。特殊變數允許一個模組A設置變數X的綁定,使另一模組B能看見並從A調用其中的X。能夠做到這一點是個優勢,能夠防止它發生也是個優勢;因此Common Lisp同時支援詞法和動態作用域兩者。
Common Lisp中的巨集是獨一無二的,和C語言中的巨集的機制相同,但是在巨集擴充的過程中由於可以使用所有現有的Common Lisp功能,因此巨集的功能就不再僅限於C語言中簡單的文字替換,而是更進階的代碼生成功能。巨集的使用形式和函數一致,但是巨集的參數在傳遞時不進行求值,而是以字面形式傳遞給巨集的參數。巨集的參數一旦傳遞完畢,就進行展開。展開巨集的過程將一直進行到這段代碼中的所有巨集都展開完畢為止。巨集完全展開完畢後,就和當初直接手寫在此處的代碼沒有區別,也就是嵌入了這段代碼上下文中,然後Lisp系統就對完整的代碼上下文進行求值。
Lisp巨集表面上類似於函數的使用,但並不是會直接被求值的表達式,它代表程式原始碼的字面轉換。巨集將包含的代碼內容當作參數,將它們綁定到巨集自身的參數,並轉換為新的原始碼形式。這個新的原始碼形式也能夠使用一個巨集,然後重複擴展,直到新的原始碼形式沒有再用到巨集。最終形式即運行時所執行的原始碼。
Lisp巨集的典型用途:
各種標準的Common Lisp功能也需要巨集來實現,如以下所列:
setf
抽象化,允許客製化編譯時賦值/存取運算子的擴展形式with-accessors, with-slots, with-open-file
,與其它相似的WITH巨集cond
是建立在基本運算子if
之上的巨集;條件分支when
和unless
也是由巨集所構成loop
迭代巨集語法
巨集是以defmacro
來定義。基本運算子macrolet
允許定義區域性的(詞法作用域)巨集。也可以使用define-symbol-macro
和symbol-macrolet
,為符號定義巨集。Paul Graham的《On Lisp》書籍詳細介紹了Common Lisp中巨集的用途。Doug Hoyte的《Let Over Lambda》書籍擴展了關於巨集的討論,聲稱「巨集是lisp編程最獨特的優勢,和任何程式語言的最大優點」。Hoyte提供了迭代開發的幾個巨集範例。
Lisp編程人員能夠利用巨集來創造新的語法形式。典型的用途是創建新的控制結構。
此處提供一個until
循環結構的巨集範例,其語法如下:
(until test form*)
until
巨集的定義:
(defmacro until (test &body body)
(let ((start-tag (gensym "START"))
(end-tag (gensym "END")))
`(tagbody ,start-tag
(when ,test (go ,end-tag))
(progn ,@body)
(go ,start-tag)
,end-tag)))
tagbody
是一個基本的Common Lisp運算子,它提供了命名標籤的能力,並使用go
形式跳轉到這些標籤。
反引號`
的用途類似單引號'
(相當於quote函數,參照形式當成資料而不求值),它還是一個可作代碼模板
的符號,其中需要求值的形式參數以逗號,
開頭填入模板;而以,@
符號為開頭的形式參數,其中巢狀的內容會
再被拆解評估。tagbody
形式測試結束條件。如果條件為真,則跳轉到結束標籤;否則執行主體的代碼,
然後跳轉到起始標記。
上述until
巨集的使用範例:
(until (= (random 10) 0)
(write-line "Hello"))
利用macroexpand-1
函數可以展開巨集的代碼。上例經過展開後的代碼如下所示:
(TAGBODY
#:START1136
(WHEN (ZEROP (RANDOM 10))
(GO #:END1137))
(PROGN (WRITE-LINE "hello"))
(GO #:START1136)
#:END1137)
在巨集展開期間,變數test
的值為(= (random (10) 0)
,變數body
的值為((write "Hello"))
,是一個列表形式。
符號通常會自動轉成英文大寫。這個TAGBODY
擴展中帶有兩個標籤符號,由GENSYM
自動產生,並且不會被拘束到任何套件中(為待綁定的暫時自由變數)。兩個go
形式會跳轉到這些標籤,因為tagbody
是Common Lisp中的基本運算子(並不是巨集),因此它沒有其它內容會再展開。展開形式中用到的when
巨集也會再展開。將一個巨集完全展開為原始碼的形式,被稱為代碼走開(code walking)。在已被完全展開的形式中,when
巨集會被基本運算子if
代換:
(TAGBODY
#:START1136
(IF (ZEROP (RANDOM 10))
(PROGN (GO #:END1137))
NIL)
(PROGN (WRITE-LINE "hello"))
(GO #:START1136))
#:END1137)
原始碼中所有包含的巨集必須在展開之後,才能正常地評估或編譯。巨集可以理解為接受和返回抽象語法樹(Lisp S-表達式)的函數。 這些函數會在求值器或編譯器調用之前,將巨集內容轉換為完整的原始碼,Common Lisp中所提供的任何運算子都可用於編寫巨集。
因為Common Lisp的巨集在展開完畢後就完全嵌入了所處的代碼上下文中,相當於以字面形式書寫同樣的代碼,因此在巨集展開代碼中與上下文代碼中相同的符號就會覆蓋上面的參照,稱為變數捕捉。如果Common Lisp的巨集展開代碼中的符號,與調用上下文中的符號相同時,通常稱為變數捕捉。對於巨集,程式設計師可在其中創建具有特殊含義的各種符號。變數捕捉這個術語可能有點誤導,因為所有的命名空間都有非預期捕捉到相同符號的弱點,包括運算子和函數的命名空間、tagbody
標籤的命名空間、catch
標記,條件處理程式和重新啟動的命名空間。
變數捕捉情況會使軟件產生缺陷,發生原因可分為下列兩種方式:
Lisp語族的Scheme方言提供了一個巨集寫入系統,它提供了參照透明度來消除這兩種類型的捕捉問題。這樣的巨集寫入系統有時被稱為「保健的」,特別是其支持者(認為不能自動解決捕捉問題的巨集系統是不正確的)。
在Common Lisp中巨集的保健,則以兩種不同方式擔保。
一種方法是使用gensym
:保證只產生唯一的符號在巨集擴展中使用,而不受到捕捉問題的威脅。在巨集定義中使用gensym
是件零瑣的雜務,但利用巨集可簡便gensym
的實例化和使用。gensym
很容易解決類型二的捕捉問題,但它們不能以相同方式來處理類型一的捕捉問題,因為巨集展開不能重新命名,周圍代碼中參照所捕捉到的介入符號(被區域定義遮蔽的全域符號)。Gensym
可以為巨集擴展所需要的全域符號,提供穩定的別名。巨集擴展使用這些秘密別名而非眾所熟知的名稱,因此重新定義熟知的名稱對巨集並沒有不利影響。
另一種方法是使用套件,在自己套件中定義的巨集,在套件中的擴展可以簡單地使用內部符號。使用套件能處理類型一和類型二捕捉問題。然而,套件不能解決參照到Common Lisp標準函數和運算子的類型一捕捉,因為用套件來解決捕捉問題,只能解析其私有符號(套件中的符號不是匯入的,或能被其它套件看見的);而Common Lisp函式庫的符號都是外部共用的,並經常匯入到用戶定義套件中,或在用戶定義套件中是可見的。
以下範例是在巨集展開時,運算子命名空間中發生的不預期捕捉:
;; expansion of UNTIL makes liberal use of DO
(defmacro until (expression &body body)
`(do () (,expression) ,@body))
;; macrolet establishes lexical operator binding for DO
(macrolet ((do (...) ... something else ...))
(until (= (random 10) 0) (write-line "Hello")))
until
巨集將展開為一個調用do
功能的形式,該形式旨在參照Common Lisp標準的do
巨集。但在這種情況下,do
可能有完全不同的含義,所以until
可能無法正常工作。
Common Lisp禁止對標準運算子和函數的重新定義,避免它們的遮蔽來解決此類問題。因為前例重新定義了do
標準運算子,實際上是一個不合格的代碼片段,Common Lisp實作應當對前例進行診斷並拒絕其重新定義。
條件系統負責Common Lisp中的異常處理。它提供條件,處理程式和重新啟動。條件是描述異常情況(例如錯誤)的物件。如果一個條件訊號被發出了,Common Lisp系統將搜尋此條件類型的處理程式並調用它。處理程式現在可以搜尋重新啟動(restart),並使用這些重新啟動之一來自動修復當前的問題,利用條件類型與條件物件的一部份所提供的任何相關資訊等,並調用相對的重新啟動函數。
如果沒有處理程式的代碼,這些重新啟動可以對用戶顯示選項(作為用戶介面的一部分,例如除錯器),讓用戶選擇和調用提供的重新啟動選項。由於條件處理程式在錯誤的上下文中被調用(堆疊仍未清空),在許多情況下對錯誤的完全回復處理是可行的,而不同於其它的異常處理系統可能已經終止了當前的執行程式。除錯器本身也可以使用*debugger-hook*
這個動態變數來客製或替換。在unwind-protect
中寫明的代碼,譬如作為終結,也會適當地被執行例外。
以下範例(使用 Symbolics Genera)中,用戶從讀取求值列印循環(REPL,即頂層)呼叫一個test函數,嘗試開啟一個檔案,而當此檔案不存在時,Lisp系統則呈現四個重新啟動的選項。用戶選擇了s-B:
這個重新啟動選項,並輸入不同的路徑名稱(以lispm-init.lisp取代了lispm-int.lisp)。用戶執行的原始碼中並沒有包含任何錯誤處理。整個錯誤處理和重新啟動代碼是由Lisp系統本身所提供,它可以處理和修復錯誤,而不終止用戶執行中的程式碼。
Command: (test ">zippy>lispm-int.lisp")
Error: The file was not found.
For lispm:>zippy>lispm-int.lisp.newest
LMFS:OPEN-LOCAL-LMFS-1
Arg 0: #P"lispm:>zippy>lispm-int.lisp.newest"
s-A, <Resume>: Retry OPEN of lispm:>zippy>lispm-int.lisp.newest
s-B: Retry OPEN using a different pathname
s-C, <Abort>: Return to Lisp Top Level in a TELNET server
s-D: Restart process TELNET terminal
-> Retry OPEN using a different pathname
Use what pathname instead [default lispm:>zippy>lispm-int.lisp.newest]:
lispm:>zippy>lispm-init.lisp.newest
...the program continues
Common Lisp包含了物件導向編程的工具包,Common Lisp物件系統或簡稱為CLOS,它是最強大的物件系統之一。Peter Norvig 解釋了在具備CLOS的動態語言中,如何使用其功能(多重繼承,混合,多方法,元類,方法組合等),以達成設計模式更簡單的實現。曾經有幾個擴展被提出來作為Common Lisp ANSI標準的物件導向編程應用,而最終採用了CLOS作為Common Lisp的標準物件系統。
CLOS是個具有多分派和多重繼承的動態物件系統,並且與靜態語言(如C++ 或Java)中的OOP設施截然不同。作為動態物件系統,CLOS允許在執行時期對泛化函數和類別進行更改。方法可以添加和刪除,類別可以添加和重新定義,物件可依照類別的變動更新,而物件所屬的類別也可以更改。CLOS已經整合到ANSI Common Lisp中。通過函數可以像普通函數一樣使用,並且是第一類資料類型。每個CLOS類別都已被整合到Common Lisp類別系統中。
Common Lisp中許多型別都有一個相對應的類別。規範中沒有說明CLOS實作的條件,CLOS進階用法的可能性並不是Common Lisp的ANSI標準,CLOS的用處有更多的潛能。一般Common Lisp實作將CLOS用於路徑名稱、流、輸入/輸出、條件,CLOS本身等等。
早期Lisp方言的幾個實現提供了直譯器和編譯器,不幸的是兩者之間語義是不同的。這些早期的Lisps在編譯器中實作了詞法作用域,在直譯器中實作了動態作用域。Common Lisp要求直譯器和編譯器兩者皆預設使用詞法作用域。Common Lisp標準描述了直譯器和編譯器的語義。可以使用compile
函數呼叫編譯器,來編譯各個函數,並使用compile-file
函數編譯原始碼檔案。Common Lisp允許類型別聲明,並提供產生編譯器代碼的選擇。後者有優化參數可選擇0(不重要)和3(最重要)之間的值:會影響到執行速度,空間,安全性,除錯和編譯速度。
還有一個函數用來評估Lisp原始碼:eval
。eval
將原始碼視為預先解析的S-表達式,而不像其它語言只當成字串處理。這樣可以用常見的Lisp函數來建構代碼,用來構造列表和符號,然後以eval
函數來評估該代碼。幾個Common Lisp實作(如Clozure CL和SBCL)以它們的編譯器來實現eval
。這樣子即使用eval
函數進行評估時,原始碼也是會被編譯。
使用compile-file
函數呼叫檔案編譯器,產生的編譯檔稱為fasl(快速載入,fast load)檔案。這些fasl檔案和原始碼檔案都能以load
功能,載入到運行的Common Lisp系統中。根據實作,檔案編譯器會產生位元組碼(例如Java虛擬機),C語言代碼(然後以C編譯器編譯)或直接使用原生機械碼。
即使原始碼已經完全被編譯,Common Lisp實作可以和用戶互動。因此,Common Lisp的互動介面並非模擬於直譯指令碼的設想。
這個語言區隔了讀取時期、編譯時期、載入時期和執行時期,並讓用戶編程在需求的步驟中,也依照這些區別來執行所需的處理種類。
有些特殊的運算子特別適合互動式開發;譬如,若defvar
還沒有任何綁定時,則只對提供給它的變數進行賦值;而defparameter
總是會執行賦值。在實時映像中互動地評估,編譯和載入代碼時,這種區別是有用的。還有一些功能也幫助撰寫編譯器和直譯器。符號由第一類物件所組成,可由用戶的代碼直接操縱。progv
基本運算子允許以編程方式創造詞法綁定,也可以運用套件。Lisp編譯器本身在運行時可用來編譯檔案或單一函數,這使得Lisp成為其它程式語言的中途編譯器或直譯器變得容易。
以下程式計算一個房間內最小數量的人,其完全獨特生日的概率小於 50%(生日悖論,1 人的概率明顯為 100%,2 為 364/365 等)。答案是 23。
;; By convention, constants in Common Lisp are enclosed with + characters.
(defconstant +year-size+ 365)
(defun birthday-paradox (probability number-of-people)
(let ((new-probability (* (/ (- +year-size+ number-of-people)
+year-size+)
probability)))
(if (< new-probability 0.5)
(1+ number-of-people)
(birthday-paradox new-probability (1+ number-of-people)))))
使用REPL呼叫函數用例:
CL-USER > (birthday-paradox 1.0 1)
23
我們定義一個人員類別和一個顯示姓名和年齡的方法。接下來,我們將一組人定義為人物物件列表。然後我們遍歷排序列表。
(defclass person ()
((name :initarg :name :accessor person-name)
(age :initarg :age :accessor person-age))
(:documentation "The class PERSON with slots NAME and AGE."))
(defmethod display ((object person) stream)
"Displaying a PERSON object to an output stream."
(with-slots (name age) object
(format stream "~a (~a)" name age)))
(defparameter *group*
(list (make-instance 'person :name "Bob" :age 33)
(make-instance 'person :name "Chris" :age 16)
(make-instance 'person :name "Ash" :age 23))
"A list of PERSON objects.")
(dolist (person (sort (copy-list *group*)
#'>
:key #'person-age))
(display person *standard-output*)
(terpri))
它以降序列印三個名字。
Bob (33)
Ash (23)
Chris (16)
使用LOOP宏:
(defun power (x n)
(loop with result = 1
while (plusp n)
when (oddp n) do (setf result (* result x))
do (setf x (* x x)
n (truncate n 2))
finally (return result)))
使用範例:
CL-USER > (power 2 200)
1606938044258990275541962092341162602522202993782792835301376
與內建的求冪函數比較:
CL-USER > (= (expt 2 200) (power 2 200))
T
WITH-OPEN-FILE
是打開一個檔案並提供一個串流的宏。在這個形式返回的時候,這個檔案自動關閉。FUNCALL
呼叫一個函數對象。LOOP
收集匹配謂詞的所有行:
(defun list-matching-lines (file predicate)
"Returns a list of lines in file, for which the predicate applied to
the line returns T."
(with-open-file (stream file)
(loop for line = (read-line stream nil nil)
while line
when (funcall predicate line)
collect it)))
函數AVAILABLE-SHELLS
呼叫上述LIST-MATCHING-LINES
函數,並以一個路徑名和作為謂詞的一個匿名函數作為參數。這個謂詞返回一個shell的路徑名或NIL
(如果這個字串不是一個shell的路徑名):
(defun available-shells (&optional (file #p"/etc/shells"))
(list-matching-lines
file
(lambda (line)
(and (plusp (length line))
(char= (char line 0) #\/)
(pathname
(string-right-trim '(#\space #\tab) line))))))
例子結果(在Mac OS X 10.6之上):
CL-USER > (available-shells)
(#P"/bin/bash" #P"/bin/csh" #P"/bin/ksh" #P"/bin/sh" #P"/bin/tcsh" #P"/bin/zsh")
Common Lisp經常和Scheme互相比較,因為它們是最受歡迎的兩種Lisp方言。Scheme早於CL,不僅來自同一個Lisp傳統,而且是Guy L. Steele與Gerald Jay Sussman設計的,Guy L. Steele也擔任過Common Lisp標準委員會的主席。
Common Lisp是一種普遍用途的的程式語言;相反的如Emacs Lisp和AutoLISP這兩種Lisp的變體,則是嵌入特定產品作為擴展用的語言。與許多早期的Lisps不同,Common Lisp(Scheme同樣)對原始碼直譯和編譯時,預設為詞法變數作用域。
大部份Lisp系統(如ZetaLisp和Franz Lisp)的設計,促成了Common Lisp在直譯器中使用動態作用域的變數,並在編譯器中使用了詞法作用域的變數。由於ALGOL 68的啟發,Scheme引入了Lisp對詞法作用域變數的單一使用;這被廣泛認同是好主意。CL也支援動態作用域的變數,但必須將其顯式聲明為「特殊」。ANSI CL直譯器和編譯器之間的作用域界定是沒有差別的。
Common Lisp有時被稱為Lisp-2,而Scheme被稱為Lisp-1。它指的是CL對函數和變數使用個別的命名空間(實際上CL有許多命名空間,例如go
標籤,block
名稱和loop
關鍵字)。在涉及多個命名空間的權衡之間,CL與Scheme倡導者之間存在着長期的爭議。在Scheme中(廣義地)必須避免與函數名稱互相衝突的變數名稱;Scheme函數通常擁有名稱為lis
,lst
或lyst
的參數,以免與系統內建的list
函數衝突。然而在CL中,在傳遞函數作為參數時一定要顯式地參照函數的名稱空間,這也是一個常見的事件,如前面小節中的排序編程範例。
在處理布爾邏輯值時,CL也與Scheme不同。Scheme使用特殊值#t和#f來表示邏輯真與假值。而CL遵循使用符號T和NIL的傳統Lisp慣例,NIL同時也是空串列。在CL中任何非NIL值被條件處理為真,例如if
;而在Scheme當中,所有非#f值被視為真。這些慣例約定允許這兩種語言的一些運算子同時作為謂詞(回應邏輯上的是非問題),並返回一個作用值進行進一步的計算,但在Scheme的布爾表達式中,等同於Common Lisp空串列的NIL值或'(),會被評估為真。
最後,Scheme的標準檔案要求尾部呼叫優化,而CL標準沒有。不過大多數CL實作會提供尾部呼叫優化,雖然通常只在程式設計師使用優化指令時。儘管如此,常見的CL編程風格並不偏好於Scheme中普遍使用的遞歸樣式- 一個Scheme程式設計師會使用尾部遞歸表達式,CL用戶則通常會用do
,dolist
,loop
等迭代表達式,或使用iterate
套件來表達。
Common Lisp是由一份技術規範定義而不是被某一種具體實現定義(前者的例子有Ada語言和C語言,後者有Perl語言)。存在很多種實現,語言標準詳細闡明了可能導致合理歧義的內容。
另外,各種實現試圖引入套件或函數庫來提供標準沒有提及的功能,可能的擴充功能如下所列:
可移植的自由軟件庫提供了各種特性,著名的有Common-Lisp.net[6]和Common Lisp Open Code Collection[7]專案。
Common Lisp設計為由增量編譯器實現。最佳化編譯的標準聲明(例如行內函數)已進入語言規範的計劃。大多數Lisp實現將函數編譯成原生的機器語言。其他的編譯器編譯為中間碼,有損速度但是容易實現二進制代碼的可移植。由於Lisp提供了互動式的提示符以及函數增量式的依次編譯,很多人誤會為Lisp是純解釋語言。
一些基於Unix的實現,例如CLISP,可以作為指令碼直譯器使用;因此,系統可以像呼叫Perl或者Unix shell直譯器一樣透明地呼叫它。
免費的可重釋出實現包括:
商業實現在Franz, Inc.[16],Xanalys Corp.[17],Digitool, Inc.[18],Corman Technologies[19]和Scieneer Pty Ltd.[20]。
Common Lisp被用於很多成功的商業應用中,最著名的(毫無疑問要歸功於Paul Graham的推廣)要數Yahoo!商店的站點。其他值得一提的例子有:
也有很多成功的開源應用用Common Lisp寫成,例如:
同樣,Common Lisp也被許多政府和非盈利組織採用。NASA中的例子有:
Seamless Wikipedia browsing. On steroids.
Every time you click a link to Wikipedia, Wiktionary or Wikiquote in your browser's search results, it will show the modern Wikiwand interface.
Wikiwand extension is a five stars, simple, with minimum permission required to keep your browsing private, safe and transparent.